package com.sunrise.xdoc.dao.impl;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigInteger;
import java.util.Collection;
import java.util.List;

import org.apache.commons.lang3.SerializationUtils;
import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import com.sunrise.xdoc.dao.IBaseDao;
import com.sunrise.xdoc.entity.AbstractIdEntity;

@Transactional
public class BaseDao<T extends AbstractIdEntity> implements IBaseDao<T> {

	@Autowired
	private SessionFactory sessionFactory;

	public SessionFactory getSessionFactory() {
		return sessionFactory;
	}

	public void setSessionFactory(SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}

	private Class<T> clazz;

	public BaseDao() {
		clazz = getEntityClass();
	}

	/**
	 * 
	 * {@inheritDoc}
	 */
	@Override
	public void setEntityClass(Class<T> clazz) {
		this.clazz = clazz;
	}

	@SuppressWarnings("unchecked")
	protected Class<T> getEntityClass() {
		if (clazz == null) {
			Type controllerType = ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
			clazz = (Class<T>) controllerType;
		}
		return clazz;
	}

	public BaseDao(Class<T> objectClazz) {
		this.clazz = objectClazz;
	}

	/**
	 * 
	 * {@inheritDoc}
	 */
	@Override
	public void delete(T entity) {
		getCurrentSession().delete(entity);
	}

	/**
	 * 
	 * {@inheritDoc}
	 */
	@Override
	public void deleteAll(Collection<T> entities) {
		for (T entity : entities) {
			getCurrentSession().delete(entity);
		}
	}

	public Session getCurrentSession() {
		// try {
		return getSessionFactory().getCurrentSession();
		// } catch (HibernateException e) {
		// return getSessionFactory().openSession();
		// }
	}

	/**
	 * 
	 * {@inheritDoc}
	 */
	@Override
	public void saveOrUpdateAll(Collection<T> entities) {
		for (T entity : entities) {
			getCurrentSession().saveOrUpdate(entity);
		}
	}

	/**
	 * The entity after saveOrUpdate contains the Id {@inheritDoc}
	 */
	@Override
	public void saveOrUpdate(T entity) {
		try {
			getCurrentSession().saveOrUpdate(entity);
		} catch (Exception e) {
			// TODO Remove
			System.out.println(e.getMessage());
		}
	}

	/**
	 * 
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	@Override
	public T findById(Long id) {
		return (T) getCurrentSession().get(getEntityClass(), id);
	}

	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	@Override
	public T findByCode(String code) {
		Criteria criteria = getCriteria();
		criteria.add(Restrictions.eq("code", code));
		List<T> list = criteria.list();
		if (list.size() > 0) {
			return list.get(0);
		}
		return null;
	}

	/**
	 * 
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	@Override
	public List<T> loadAll() {
		Criteria criteria = getCriteria();
		criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
		return criteria.list();
	}

	// @Override
	// public List<T> findAllActive() {
	// List<T> entities = findAll();
	// List<T> removedList = new ArrayList<>();
	// if (IActiveObject.class.isAssignableFrom(getEntityClass())) {
	// for (T entity : entities) {
	// if (!((IActiveObject) entity).isActive()) {
	// removedList.add(entity);
	// }
	// }
	// }
	// entities.removeAll(removedList);
	// return entities;
	// }

	/**
	 * 
	 * {@inheritDoc}
	 */
	@Override
	public DetachedCriteria getDetachedCriteria() {
		return DetachedCriteria.forClass(getEntityClass());
	}

	private Criteria getCriteria() {
		return getCurrentSession().createCriteria(getEntityClass());
	}

	/**
	 * 
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	@Override
	public List<T> findByCriteria(DetachedCriteria criteria, int firstResult, int maxResults) {
		Criteria executableCriteria = getCriteria(criteria);
		if (firstResult >= 0) {
			executableCriteria.setFirstResult(firstResult);
		}
		if (maxResults > 0) {
			executableCriteria.setMaxResults(maxResults);
		}
		return executableCriteria.list();
	}

	/**
	 * 
	 * {@inheritDoc}
	 */
	@Override
	public List<T> findByCriteria(DetachedCriteria criteria) {
		return findByCriteria(criteria, -1, -1);
	}

	/**
	 * 
	 * {@inheritDoc}
	 */
	@Override
	public T findFirstByCriteria(DetachedCriteria criteria) {
		List<T> list = findByCriteria(criteria, 0, 1);
		if (list.size() > 0) {
			return list.get(0);
		}
		return null;
	}

	/**
	 * This implements for MySQL DB. Use the supported function if using other
	 * DB. </br> {@inheritDoc}
	 */
	@Override
	public long getNextSequence(final String name) {
		Session session = getCurrentSession();
		Query query = session.createSQLQuery("select seq('" + name + "')");
		long uniqueResult = ((BigInteger) query.uniqueResult()).longValue();
		if (uniqueResult == 0) {
			query = session.createSQLQuery("insert into `seq` values('" + name + "', 1)");
			query.executeUpdate();
			uniqueResult = 1;
		}
		return uniqueResult;
	}

	/**
	 * Counts the number of results of a search.
	 * 
	 * @param criteria
	 *            The criteria for the query.
	 * @return The number of results of the query.
	 * @throws DataAccessException
	 */
	@Override
	public long findCountByCriteria(final DetachedCriteria criteria) throws DataAccessException {

		Assert.notNull(criteria, "DetachedCriteria must not be null");
		Criteria executableCriteria = getCriteria(criteria);
		executableCriteria.setProjection(Projections.rowCount());
		Long result = (Long) executableCriteria.uniqueResult();
		if (result == null) {
			result = 0L;
		}
		return result;
	}

	private Criteria getCriteria(DetachedCriteria criteria) {
		DetachedCriteria clonedCriteria = (DetachedCriteria) SerializationUtils.clone(criteria);
		Criteria executableCriteria = clonedCriteria.getExecutableCriteria(getCurrentSession());
		return executableCriteria;
	}

	@Override
	public long getRowCount() {
		return findCountByCriteria(getDetachedCriteria());
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void flush() {
		getCurrentSession().flush();
	}
}